S05-01 多线程-多线程
[TOC]
概述
进程与线程
进程(Process):操作系统分配资源(如内存)的最小单位。一个独立运行的程序就是一个进程(比如你打开的一个浏览器)。线程(Thread):CPU 调度的最小单位。它是进程中的一个执行流程。一个进程可以包含多个线程,这些线程共享该进程的内存资源(如堆内存和方法区),但每个线程有自己独立的程序计数器(PC)和虚拟机栈。
一句话总结: 进程就像是一辆火车,而线程就是火车上的一节节车厢。多线程就是让这列火车有多个车厢同时运作。
图示:JVM 中的线程
不同的进程之间是不共享内存的。
每个线程独立的拥有自己的:虚拟机栈、本地方法栈、程序计数器。一个进程中的多个线程,共享进程的:方法区、堆。
进程之间的数据交换和通信的成本很高。

并发与并行
这两个概念经常被混淆,但它们在计算机科学中有着明确的区别:
并发 (Concurrency):指的是系统拥有处理多个任务的能力,但不一定是同时执行。在单核 CPU 中,操作系统通过快速切换时间片,让多个线程交替执行,使得宏观上看起来像是在“同时”运行。并行 (Parallelism):指的是系统拥有同时执行多个任务的能力。这通常需要多核 CPU 的支持,真正的在同一物理时刻,不同的核心在执行不同的线程。
比喻:
- 并发是一个咖啡机,两个人排队交替接咖啡;
- 并行是两台咖啡机,两个人同时各自接咖啡。

示例:查看 CPU 核数
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
int cores = runtime.availableProcessors();
System.out.println("CPU 核心数 = " + cores); // 16
}线程调度
在 Java 多线程编程中,当有多个线程处于可运行(RUNNABLE)状态时,谁先执行、谁后执行、每个线程执行多久,这就涉及到了线程调度(Thread Scheduling)。
理解线程调度,有助于我们明白为什么多线程程序的执行结果往往是“不可预测”的,以及我们能在多大程度上干预这种调度。
在计算机科学中,主要有两种线程调度模型:
协同式调度
方式一:协同式调度 (Cooperative Scheduling):
- 机制: 线程的执行时间由线程本身控制。一个线程执行完自己的工作后,主动通知系统切换到另外一个线程。
- 优点: 实现简单,没有线程同步的问题(因为什么时候切换是确定的)。
- 缺点: 极其危险。如果一个线程编写有问题,一直不让出 CPU(比如死循环),会导致整个系统崩溃。
抢占式调度
方式二:抢占式调度 (Preemptive Scheduling) —— Java 的选择:
- 机制: 线程的执行时间由操作系统(调度器) 来分配。操作系统会给每个线程分配一个“时间片”(Time Slice,通常是几十毫秒)。时间片用完,或者发生阻塞时,操作系统会强制剥夺该线程的 CPU 执行权,并把 CPU 交给其他处于就绪状态的线程。
- 优点: 一个线程的阻塞或死循环不会导致整个系统崩溃,多任务并发性好。
- 缺点: 线程切换频繁会有上下文切换(Context Switch)开销;会导致线程安全问题,需要开发者手动进行同步(加锁等)。
核心结论: Java 的线程调度是抢占式的。这意味着 Java 程序无法绝对控制哪个线程在什么时刻执行,只能对调度器提出“建议”。

线程优先级
线程优先级 (Thread Priority):
既然调度由 OS 决定,Java 提供了 setPriority(int newPriority) 方法,试图给调度器一些“建议”。
Java 线程优先级范围是 1 到 10:
Thread.MIN_PRIORITY(1)Thread.NORM_PRIORITY(5) —— 默认优先级Thread.MAX_PRIORITY(10)
优先级规则与致命陷阱:
高优先级不等于先执行: 优先级高的线程只是获取 CPU 时间片的概率更大,绝不意味着它一定会在低优先级线程之前执行完毕。
OS 映射差异: Java 有 10 个优先级,但底层操作系统可能没有这么多。比如 Windows 有 7 个,Linux 的某些调度策略下优先级可能完全被忽略。多个 Java 优先级可能被映射到同一个 OS 优先级上。
饥饿 (Starvation): 如果你把某个线程优先级设得极低,在 CPU 繁忙时,它可能永远抢不到时间片,导致“饿死”。
实战建议: 在实际的业务开发中,绝对不要依赖线程优先级来控制程序的业务逻辑和执行顺序! 它极不可靠。通常我们都保持默认的优先级(5)即可。
单核和多核
单核CPU:在一个时间单元内,只能执行一个线程的任务。
例如,可以把CPU看成是医院的医生诊室,在一定时间内执行一行代码(给一个病人诊断治疗)。所以单核CPU就是,代码经过前面一系列的前导操作(类似于医院挂号),然后到cpu处执行时发现,就只有一个CPU,大家排队执行。(类似于10个挂号窗口挂号,结果跑到医生那只有一个医生,只能排队等)。
这时候想要提升系统性能,只有两个办法,要么提升CPU性能(让医生看病快点),要么多加几个CPU(多整几个医生)。后者即为提供多核CPU。如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)。
问题:多核的效率是单核的倍数吗?
譬如4核A53的cpu,性能是单核A53的4倍吗?理论上是,但是实际不可能,至少有两方面的损耗。
- 一个是多个核心的其他共用资源限制。譬如,4核CPU对应的内存、cache、寄存器并没有同步扩充4倍。这就好像医院一样,1个医生换4个医生,但是做B超检查的还是一台机器,性能瓶颈就从医生转到B超检查了。
- 另一个是多核CPU之间的协调管理损耗。譬如多个核心同时运行两个相关的任务,需要考虑任务同步,这也需要消耗额外性能。好比公司工作一样,一个人的时候至少不用开会浪费时间,自己跟自己商量就行了。两个人就要开会同步工作,协调分配,所以工作效率绝对不可能达到2倍。
线程创建方式
严谨地说,Java 中创建线程本质上只有一种方式:构造一个 java.lang.Thread 类的实例,并调用其 start() 方法。所谓的“多种方式”,其实是指封装线程执行任务(Task)的方式不同。
以下是 Java 中常见的四种创建并启动线程的方式详解:
方式1:继承 Thread 类
这是最直观、最古老的方式。你只需要创建一个类继承 Thread,并重写它的 run() 方法。
代码示例:
// 1. 自定义类继承 Thread
class MyThread extends Thread {
// 2. 重写 Thread 类的 run 方法
@Override
public void run() {
// 线程要执行的业务逻辑
System.out.println(Thread.currentThread().getName() + " 正在执行...");
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 3. 创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
// 4. 调用 start() 方法启动线程
t1.start();
t2.start();
}
}- 优点: 编写简单,如果在
run()方法内部需要获取当前线程,直接使用this即可,无须调用Thread.currentThread()。 - 缺点: 极其不推荐在现代开发中使用。因为 Java 是单继承的,如果你的类已经继承了
Thread,就无法再继承其他业务类,大大限制了代码的扩展性。此外,任务(run方法)和线程(Thread对象)强耦合在一起。
练习:
创建一个分线程1,用于遍历 100 以内的偶数

创建两个分线程,一个线程用于遍历 100 以内的偶数,另一个线程用于遍历 100 以内的奇数
方式一:使用标准方法

方式二:使用匿名类的方式

方式2:实现 Runnable 接口
实现 :Runnable 接口
为了解决单继承的限制,Java 提供了 Runnable 接口。你将需要执行的任务写在 Runnable 实现类的 run() 方法中,然后将这个实现类作为一个“目标任务”丢给 Thread 对象去执行。
写法一:标准写法:
// 1. 实现 Runnable 接口
class MyRunnable implements Runnable {
// 2. 实现 Runnable 接口的 run() 方法
@Override
public void run() {
// 线程要执行的业务逻辑
System.out.println(Thread.currentThread().getName() + " 正在执行 Runnable 任务...");
}
}
public class RunnableDemo {
public static void main(String[] args) {
// 3. 创建当前任务对象
MyRunnable task = new MyRunnable();
// 4. 将任务对象传入 Thread 构造器,并创建 Thread 类的实例对象
Thread t1 = new Thread(task, "线程-A");
Thread t2 = new Thread(task, "线程-B");
// 5. 调用 start() 方法启动线程
t1.start();
t2.start();
}
}写法二:匿名实现类写法:
public class RunnableDemo {
public static void main(String[] args) {象
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 线程要执行的业务逻辑
System.out.println(Thread.currentThread().getName() + " 正在执行 Runnable 任务...");
}
}, "线程-A").start();
}
}写法三:现代写法(使用 Lambda 表达式,Java 8+):
Thread t3 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 使用 Lambda 执行任务...");
}, "线程-C");
t3.start();优缺点:
- 优点
- 缺点:
run()方法没有返回值,也无法抛出受检异常(Checked Exception),只能在方法内部try-catch。
注意:Thread类实际上也是实现了Runnable接口的类(代理模式):
javapublic class Thread extends Object implements Runnable
练习:
程序阅读

方式3:实现 Callable 接口与 FutureTask
API:FutureTask 类
FutureTask API:
构造方法:
- FutureTask():
(Callable<V> callable),构造方法,将一个带有返回值的Callable任务包装成FutureTask。 - FutureTask():
(Runnable runnable, V result),构造方法,包装一个Runnable任务,并提前指定好一个返回值result。
实例方法:
Vget():(),获取计算结果(死等派)。
极其重要:如果任务还没执行完,调用这个方法的当前线程会被完全阻塞挂起,直到任务执行完毕返回结果,或者抛出异常。Vget():(long timeout, TimeUnit unit),获取计算结果(限时派,极度推荐)。
如果到了指定时间任务还没出结果,会抛出TimeoutException。在企业开发中,强烈禁止使用无参的get()以防止主线程被永远卡死。
实现 Callable 接口与 FutureTask
如果你希望线程执行完毕后能返回一个结果,或者能抛出异常供外部捕获,那么就需要使用 Callable 接口(Java 5 引入)。
因为 Thread 类的构造器只接受 Runnable,不接受 Callable,所以我们需要一个桥梁——FutureTask。FutureTask 实现了 Runnable 接口,同时它的构造器可以接收 Callable。
代码示例:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
// 1. 实现 Callable 接口,泛型 <Integer> 代表返回值的类型
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + " 正在计算...");
Thread.sleep(2000); // 模拟耗时操作
return 100 + 200;
}
}
public class CallableDemo {
public static void main(String[] args) throws Exception {
// 2. 创建 Callable 任务
MyCallable task = new MyCallable();
// 3. 使用 FutureTask 包装 Callable
FutureTask<Integer> futureTask = new FutureTask<>(task);
// 4. 将 FutureTask 交给 Thread 执行
Thread t1 = new Thread(futureTask, "计算线程");
t1.start();
System.out.println("主线程可以继续做其他事情...");
// 5. 获取结果。注意:get() 方法会阻塞当前主线程,直到 call() 执行完毕并返回结果!
Integer result = futureTask.get();
System.out.println("计算结果是: " + result);
}
}- 优点: 功能最强大,有返回值,能抛出异常。通过
FutureTask还可以取消任务、判断任务是否完成。 - 缺点: 代码相对繁琐。调用
get()方法时如果没有设置超时时间,可能会导致阻塞。
方式4:使用线程池(推荐)@
使用线程池(Executor 框架):
在实际的企业级项目开发中,我们几乎从来不会手动去 new Thread()。因为频繁创建和销毁线程会消耗极大的系统资源,并且难以统一管理,容易导致内存溢出(OOM)。
业界标准做法是使用线程池。你只需要把 Runnable 或 Callable 任务提交给线程池,线程池会自动分配工作线程来执行它们。
代码示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadPoolDemo {
public static void main(String[] args) throws Exception {
// 1. 创建一个固定大小为 3 的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 2. 提交 Runnable 任务 (使用 execute)
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行无返回值的任务");
});
// 3. 提交 Callable 任务 (使用 submit)
Future<String> future = threadPool.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 执行有返回值的任务");
return "Task Success";
});
System.out.println("Callable 返回值: " + future.get());
// 4. 关闭线程池 (不再接受新任务,等待已有任务执行完毕)
threadPool.shutdown();
}
}优点:
- 资源复用:避免频繁创建销毁线程。
- 响应速度快:任务来了直接拿池子里的空闲线程执行。
- 便于管理:可以控制最大并发数,提供定时执行、周期执行等功能。
底层原理
start() 源码
在 Java 中,启动一个线程我们总是调用 thread.start(),而不是直接调用 run()。你可能听过无数次“start() 才会真正创建新线程”,但它在底层究竟玩了什么魔法?
为了彻底看清它的真面目,我们需要跨越 Java 源码层 -> JVM 桥接层(C++) -> 操作系统内核层(OS)。这里我们主要看 Java 源码层,其他的略过(过于硬核)。
当我们调用 thread.start() 时,Java 层面做的事情相对简单,主要负责状态拦截和线程组管理。
public synchronized void start() {
// 1. 状态校验:如果线程状态不是 NEW (threadStatus == 0),直接抛出异常
// 这也是为什么同一个线程对象不能被 start() 两次的原因
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 2. 将当前线程加入到对应的线程组(ThreadGroup)中
group.add(this);
boolean started = false;
try {
// 3. 核心:调用本地方法 start0(),真正的大招在这里
start0();
started = true;
} finally {
try {
if (!started) {
// 如果启动失败,通知线程组进行清理(回滚计数等)
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* 忽略异常,让 start0 抛出的原始异常继续向上抛 */
}
}
}
// 这是一个 native 本地方法,由 C/C++ 实现,并由 JVM 调用
private native void start0();start0() 是一个 native 本地方法,由 C/C++ 实现,并由 JVM 调用。它其实才是真正创建线程的方法。
调用 start0() 后,Java 向上打了个报告,JVM 向操作系统申请了一套真正的线程资源,并把你的 run() 方法作为新线程的启动任务交给了操作系统去执行。
代理模式实现 Runable
在 Java 的多线程体系中,通过实现 Runnable 接口来创建线程是一种极其推荐的做法。而在这一设计的背后,隐藏着一个非常经典的面向对象设计模式——静态代理模式(Static Proxy Pattern)。
可以说,Thread 类和 Runnable 接口之间的关系,就是教科书级的代理模式实现。
代理模式
在设计模式中,代理模式的定义是:为其他对象提供一种代理以控制对这个对象的访问。
- 代理类和真实处理业务的类实现同一个接口。
- 代理类不仅包含真实角色的功能,还可以进行功能增强(扩展)。
静态代理模式通常包含三个核心角色:
抽象角色(Subject):定义了代理角色和真实角色共同的接口(
Runable)。真实角色(Real Subject):真正执行核心业务逻辑的角色(
MyTask)。代理角色(Proxy Subject):内部持有真实角色的引用,负责在执行真实角色逻辑的前后,进行额外的功能增强(
Thread)。
线程中的代理模式角色
当我们写下如下代码时,这三个角色就已经各就各位了:
// 1. 真实角色:专注于业务逻辑
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("真正的业务逻辑正在由 " + Thread.currentThread().getName() + " 执行");
}
}
public class Main {
public static void main(String[] args) {
// 1. 创建真实角色
MyTask task = new MyTask();
// 2. 创建代理角色,并将真实角色作为参数注入
Thread thread = new Thread(task);
// 3. 通过代理角色启动
thread.start();
}
}它们是如何一一对应的呢?
抽象角色 ->
Runnable接口:它只定义了一个
public void run()方法。无论是你的业务代码,还是底层的线程调度,大家都要遵守这个规范。真实角色 -> 程序员自定义的实现类(如
MyTask):它实现了
Runnable接口,里面写的是你真正想让线程去执行的核心业务代码。它只关心“做什么”,而不关心“怎么分配 CPU 资源”。代理角色 ->
Thread类:你可能没注意到,
Thread类本身也实现了Runnable接口(核心源码:public class Thread implements Runnable)。它负责“功能增强”,去处理复杂的底层线程创建、CPU 资源申请和销毁等工作。
代理模式运作机制
我们来看一下 Thread 类的内部源码,看看它是如何完美符合静态代理模式的:
代理类内部持有真实角色的引用:
在 Thread 类中,有一个私有的成员变量 target,用来接收我们传进去的真实任务:
public class Thread implements Runnable {
/* What will be run. */
private Runnable target; // 真实角色的引用
// 构造函数:注入真实角色
public Thread(Runnable target) {
this.init(null, target, "Thread-" + nextThreadNum(), 0);
}
// 内部初始化方法,最终将我们传进来的 task 赋值给 target
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
// ... 省略其他初始化代码
this.target = target;
}
}代理类的 :run() 方法进行内部转发
当底层操作系统分配好 CPU 资源,开始执行 Thread 的 run() 方法时,它其实是转发给了 target(也就是我们的真实角色):
@Override
public void run() {
// 完美的代理转发逻辑!
// 如果传了真实角色(target),就调用真实角色的 run() 方法
if (target != null) {
target.run();
}
}代理模式优点
如果直接继承 Thread 类也能创建线程,为什么 JDK 还要设计 Runnable 并采用代理模式呢?这种设计带来了巨大的架构优势:
彻底的解耦
- 真实角色(
Runnable):只关心业务逻辑(Task)。 - 代理角色(
Thread):只关心线程调度和生命周期管理(Worker)。 - 如果业务逻辑改变了,你只需要修改
Runnable的实现,不需要动任何线程控制的代码;反之,JVM 如果优化了线程调度(比如引入虚拟线程),你的业务代码也完全不需要改动。
- 真实角色(
绕过 Java 的单继承限制
Java 类只能继承一个父类。如果你直接
extends Thread,这个类就不能再继承其他类了(比如无法继承BaseService)。而采用代理模式,你的类只需要
implements Runnable,依然保留了继承其他类的权利。方便资源共享
代理模式使得同一个真实角色(
Runnable)可以被多个代理角色(Thread)共同挂载。javaMyTask task = new MyTask(); // 只有一份真实数据/逻辑 // 多个代理同时执行这一个任务,天然适合资源共享(如买票系统、线程池池化技术) new Thread(task, "窗口 A").start(); new Thread(task, "窗口 B").start();
总结
Java 通过 Runnable 接口创建线程的本质,就是通过 Thread 这个代理类,给你的 Runnable 真实任务套上了一层“能够被操作系统调度”的精美外壳。你负责编写纯粹的业务逻辑(真实角色),Thread 负责解决复杂的线程并发问题(代理角色),两者相得益彰。
练习:MiniThread
需求:实现一个 mini 的代理模式
package p01_create_thread.d02_create_thread;
public class D05E02MiniThread {
public static void main(String[] args) {
// 1. 方式一:
MyTask task = new MyTask();
new MiniThread(task).start();
// 2. 方式二:
new MiniThread(new Runnable() {
@Override
public void run() {
System.out.println("匿名 MiniThread 正在运行...");
}
}).start();
}
}
/**
* Task 类
*/
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("MiniThread 正在运行...");
}
}
/**
* Thread 类
*/
class MiniThread implements Runnable {
private Runnable target;
public MiniThread(Runnable task) {
this.target = task;
}
@Override
public void run() {
if(target != null) {
target.run();
}
}
public void start() {
// 进行一系列检测
start0();
}
private void start0() {
run();
};
}Thread~
是 Java 中进行多线程编程的最核心、最基础的类。在 Java 的世界里,任何代码的执行最终都是由 java.lang.ThreadThread 类的实例来驱动的。
理解 Thread 类的内部结构、构造方式以及它提供的丰富方法,是掌控 Java 并发编程的必经之路。
以下是对 Thread 类的全面解剖:
类的声明与本质
类的声明与本质:
打开 JDK 源码,你可以看到 Thread 类的声明如下:
public class Thread implements Runnable {
// ...
}这说明了一个非常重要的设计:Thread 类本身也实现了 Runnable 接口。
这意味着,Thread 不仅是线程的“驱动器”(负责与底层操作系统交互并分配 CPU 资源),它自己也可以被看作是一个包含了 run() 方法的“任务”。当我们直接继承 Thread 类并重写 run() 方法时,其实就是把“驱动器”和“任务”绑定在了一起。
API:Thread 类
属性
每个 Thread 对象在底层都有几个极其关键的属性来标识它的状态和特征:
属性:
Stringname:线程名称。每个线程都有一个名字。如果没有手动指定,Java 会自动生成类似Thread-0,Thread-1的名字。
在实际开发中,强烈建议给线程起一个有业务意义的名字,这在排查日志和死锁时是救命的。intpriority:线程优先级。范围从 1 (MIN_PRIORITY) 到 10 (MAX_PRIORITY),默认值是 5 (NORM_PRIORITY)。
注意:优先级高的线程理论上获取 CPU 时间片的概率更大,但这完全取决于操作系统的具体实现。在 Java 开发中,绝对不能依赖线程优先级来控制业务逻辑的先后顺序。booleandaemon:是否为守护线程。分为用户线程(User Thread)和守护线程(Daemon Thread)。Runnabletarget:目标任务。这就是你在调用new Thread(Runnable task)时传进去的那个任务对象。
构造方法
Thread 类提供了多个重载的构造方法,最常用的有以下几个:
构造方法:
Thread():
(),创建一个新的线程对象,名称自动生成。Thread():
(String name),创建具有指定名称的线程对象。Thread():
(Runnable target),将一个Runnable任务对象传递给线程,由线程负责执行该任务。Thread():
(Runnable target, String name),最推荐的用法:同时指定要执行的任务和线程名称。
静态方法
为了便于记忆,我们可以将 Thread 类的方法分为三类:静态工具方法、线程控制方法和属性获取/设置方法。
静态方法:作用于当前正在执行的线程
ThreadThread.currentThread():(),极度常用。返回对当前正在执行的线程对象的引用。常用于获取当前线程的名称:Thread.currentThread().getName()。voidThread.sleep():(long millis),让当前线程休眠(暂停执行)指定的毫秒数。进入TIMED_WAITING状态。注意:休眠期间不会释放已经持有的任何对象锁。voidThread.yield():(),线程礼让。当前线程主动提示调度器自己愿意让出 CPU 的使用权,状态由运行中(Running)变为就绪(Ready)。但操作系统可以选择忽略这个提示。
实例方法:线程控制
实例方法:线程控制:
voidstart():启动线程。通知 Java 虚拟机为其分配系统资源,并在就绪后调用该线程的run()方法。一个线程的start()方法只能被调用一次,否则抛IllegalThreadStateException。voidrun():线程要执行的具体的业务代码实体。如果直接调用它,它会被当作当前线程下的一个普通方法执行,不会启动新线程。voidjoin():线程插队。比如在主线程中调用t1.join(),那么主线程会一直阻塞,直到t1线程执行完毕。常用于等待其他线程的计算结果。voidinterrupt():中断线程。它并不会粗暴地立即停止线程,而是给目标线程打上一个“中断标记”。目标线程需要配合检查这个标记来决定是否安全地退出。booleanisInterrupted():检查中断标记。测试该线程是否已经被中断。booleanisAlive():测试该线程是否处于活动状态(已经start()且尚未终止)。
练习:线程插队【
main 线程创建一个子线程,每隔1s输出hello, 输出20次, 主线程每隔1秒, 输出hi, 输出20次.要求: 两个线程同时执行, 当主线程输出5次后, 就让子线程运行完毕, 主线程再继续。
public class D01_Thread_Join {
public static void main(String[] args) throws InterruptedException {
// 1. 创建子线程,每隔1s输出hello, 输出20次
Thread subThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " Say: Hello" + (i+1));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "subThread");
subThread.start();
// 2. 主线程每隔1s输出hi, 输出20次
for (int i = 0; i < 20; i++) {
if(i == 5) {
// 3. 线程插队
subThread.join();
}
System.out.println(Thread.currentThread().getName() + " Say: hi" + (i+1));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}实例方法:属性控制
实例方法:属性控制:
final StringgetName()final voidsetName():(String name),获取/设置线程名。final intgetPriority()final voidsetPriority():(int newPriority),获取/设置优先级。final booleanisDaemon()final voidsetDaemon():(boolean on),将该线程标记为守护线程或用户线程。必须在start()方法之前调用,否则抛出异常。
注意事项
start() vs run()
误区一::start() 和 run() 的天壤之别
- 调用
start():真正向操作系统申请创建了一个新的本地线程,新线程启动后会自动去执行run()里的代码。这叫多线程。 - 调用
run():仅仅是在当前线程中执行了一个名为run的普通对象方法而已,根本没有创建新线程。这叫同步执行。
练习:sleep()
题目:如下的代码中 sleep() 执行后,哪个线程进入了阻塞状态?

回答:主线程进入了阻塞状态。
守护线程
在 Java 的多线程世界里,线程被分为两大阵营:用户线程(User Thread)和守护线程(Daemon Thread)。如果把用户线程比作在台前表演的“主角”,那么守护线程就是默默在幕后打杂的“后勤人员”。
什么是守护线程
守护线程(Daemon Thread,后台线程) 唯一目的就是为其他非守护线程(用户线程)提供便利服务。
JVM 的退出的生死判据:一个 Java 程序的生命周期与用户线程紧密相连。只要还有任何一个用户线程在运行,JVM 就不会退出(比如你的
main线程就是一个典型的用户线程)。自动寿终正寝:当程序中的所有用户线程都执行完毕退出后,即使守护线程里的代码还在疯狂循环,JVM 也会毫不留情地直接关闭,所有的守护线程都会被立即强行终止。
经典比喻:守护线程就像是游戏里的“召唤物”或操作系统的“后台常驻进程”。当英雄(用户线程)阵亡或通关(全部退出)时,召唤物(守护线程)不管手头活干没干完,都会瞬间凭空消失。
应用场景
在 Java 底层和日常开发中,守护线程经常被默默使用:
JVM 垃圾回收机制(GC):这是最著名的守护线程。它在后台默默监控内存,一旦发现垃圾就回收。当你的业务程序(用户线程)都跑完了,GC 也就没有存在的意义了,随 JVM 一同退出。
后台监控与资源清理:例如,在应用启动时开启一个线程,每隔 5 分钟自动清理一次本地临时文件或内存缓存。这种定时任务极其适合设置为守护线程,因为它们不应该阻止主程序的正常关闭。
心跳检测包发送:在 RPC 框架(如 Netty、Dubbo)或分布式系统中,用于维持长连接、定期向服务器发送心跳包的线程,通常也是守护线程。
创建守护线程
在 Java 中,任何一个通过 new Thread() 创建的线程,默认都是用户线程。要将其变成守护线程,需要在 start() 方法调用前通过 setDaemon(boolean on) 方法进行显式设置。
public class DaemonThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread daemonTask = new Thread(() -> {
while (true) {
try {
System.out.println("守护线程:我在后台默默为你服务...");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 【核心】将该线程标记为守护线程
daemonTask.setDaemon(true); // 注意:必须在线程调用 start() 方法之前设置
daemonTask.start(); // 启动线程
// 主线程(用户线程)只运行 2 秒钟
System.out.println("主线程:我要开始执行核心业务了...");
Thread.sleep(2000);
System.out.println("主线程:我的业务执行完毕,准备退出!");
// 当主线程退出后,daemonTask 也会被 JVM 强行终止,控制台不会再打印
}
}守护线程 vs 用户线程
| 特性 | 用户线程 (User Thread) | 守护线程 (Daemon Thread) |
|---|---|---|
| 默认状态 | 手动创建的线程默认为用户线程 | 必须显式调用 setDaemon(true) 进行转换 |
| 职责定位 | 负责执行核心业务逻辑 | 辅助用户线程,提供后台支持(如 GC、清理) |
| 对 JVM 寿命的影响 | 决定 JVM 的生死。只要有一个未结束,JVM 就不会停止 | 无法阻止 JVM 退出。用户线程死光,它立刻陪葬 |
| 生命周期 | 由自身的业务逻辑(run 方法)决定何时结束 | 随时可能在任意一行代码执行时被 JVM 强行掐断 |
注意事项
如果你决定在项目中使用守护线程,必须时刻警惕以下两个大坑:
setDaemon()的调用时机不对:铁律:
setDaemon(true)必须在线程调用start()方法之前设置!如果你先写了
thread.start(),然后才写thread.setDaemon(true),程序不仅不会变成守护线程,还会直接无情地抛出IllegalThreadStateException异常。finally块中的代码可能永远不会执行:这是初学者最容易踩的编译期和运行期认知不一致的坑。在 Java 的常规认知中,
finally块里的代码是“铁律执行”的。但是在守护线程中,这一铁律被彻底打破!javaThread daemonThread = new Thread(() -> { try { System.out.println("守护线程启动,开始写数据..."); Thread.sleep(5000); // 模拟耗时的文件写入操作 } finally { // 如果在 sleep 期间,所有的用户线程都退出了 // JVM 会直接关闭,下面这行代码压根没有执行的机会! System.out.println("【危险】释放文件句柄、关闭数据库连接..."); } }); daemonThread.setDaemon(true); daemonThread.start();避坑药方:绝对不要在守护线程中去执行任何需要严谨释放资源的操作
释放资源的操作有关闭输入输出流、释放数据库连接、完成重大的文件落盘写入等。因为你无法预测它什么时候会突然死亡。这类需要安全收尾的任务,请老老实实交给用户线程去处理。
线程生命周期
要真正写出健壮的并发程序,或者在生产环境中排查 CPU 飙高、程序卡死(死锁)等问题,深刻理解 Java 线程的生命周期是必不可少的基本功。
API:Thread.State 枚举
在 Java 中,线程的生命周期并不是由操作系统直接决定的,而是由 JVM 明确规定在 java.lang.Thread.State 枚举类中的 6 种状态。
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}线程的生命周期状态:
NEW:新建。线程对象已经被创建出来,但是还没有调用
start()方法。- 状态解析: 此时它只是堆内存中的一个普通的 Java 对象,操作系统内核中还没有对应的底层线程。
- 代码场景:
Thread t = new Thread();
RUNNABLE:可运行。线程已经调用了
start()方法,随时准备好执行,或者正在执行中。- 状态解析: 注意,这是一个极易误解的点! 在操作系统的层面,线程分为“就绪(Ready,等待 CPU 分配时间片)”和“运行(Running,正在 CPU 上执行)”。但在 Java 的世界里,JVM 将这两种状态合并统称为
RUNNABLE。因此,处于RUNNABLE状态的线程可能正在疯狂运行,也可能在排队等 CPU。 - 状态流转:
NEW-> 调用t.start()->RUNNABLE。
- 状态解析: 注意,这是一个极易误解的点! 在操作系统的层面,线程分为“就绪(Ready,等待 CPU 分配时间片)”和“运行(Running,正在 CPU 上执行)”。但在 Java 的世界里,JVM 将这两种状态合并统称为
BLOCKED:阻塞。线程试图获取一个内部的对象锁(Monitor Lock),但该锁正被其他线程持有,因此当前线程被阻塞。
- 状态解析: 在 Java 中,只有在等待进入
synchronized代码块或方法时,线程才会进入BLOCKED状态。等待java.util.concurrent.locks.Lock(如ReentrantLock)时,线程进入的是WAITING状态,而不是BLOCKED。 - 状态流转:
RUNNABLE-> 竞争synchronized锁失败 ->BLOCKED。BLOCKED-> 成功获取到锁 ->RUNNABLE。
- 状态解析: 在 Java 中,只有在等待进入
WAITING:等待。线程进入一种“无限期等待”的状态。它放弃了 CPU 的使用权,并且不会自动醒来,必须等待另一个线程执行特定的唤醒操作。
- 状态解析: 处于这种状态的线程通常是在等待某个条件成立。
- 触发条件(从 RUNNABLE 变为 WAITING):
- 调用了没有设置超时时间的
Object.wait()。 - 调用了没有设置超时时间的
Thread.join()(本质上也是调用的wait())。 - 调用了
LockSupport.park()(JUC 锁的底层基石)。
- 调用了没有设置超时时间的
- 唤醒条件(从 WAITING 变为 RUNNABLE):
- 其他线程调用了该对象的
Object.notify()或Object.notifyAll()。 LockSupport.unpark(Thread)被调用。
- 其他线程调用了该对象的
TIMED_WAITING:计时等待。类似于
WAITING,但它是“限时等待”。线程等待一段时间,如果时间到了还没有被唤醒,它会自动醒来并尝试继续执行。- 触发条件(从 RUNNABLE 变为 TIMED_WAITING):
Thread.sleep(long millis)(最常见,抱着锁睡觉)。- 带有超时参数的
Object.wait(long timeout)。 - 带有超时参数的
Thread.join(long millis)。 LockSupport.parkNanos()或LockSupport.parkUntil()。
- 唤醒条件(从 TIMED_WAITING 变为 RUNNABLE):
- 等待时间到达。
- 提前被
notify()或notifyAll()唤醒。
- 触发条件(从 RUNNABLE 变为 TIMED_WAITING):
TERMINATED:终止。线程的生命周期走到了尽头,已经停止运行。
- 状态解析: 一旦线程进入
TERMINATED状态,就绝对不可能再复活。如果你尝试对一个已终止的线程再次调用start()方法,会抛出IllegalThreadStateException异常。 - 触发条件:
run()方法中的代码正常执行完毕。- 线程在执行过程中抛出了一个未捕获的异常(Exception 或 Error),导致线程意外死亡。
- 状态解析: 一旦线程进入
示例:线程状态演示
package p01_create_thread.d04_lifecycle;
public class D01_ThreadState {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
// 1. 查看线程状态
System.out.println("1. " + myThread.getName() + " 线程此时的状态为:" + myThread.getState()); // NEW
myThread.start();
while(Thread.State.TERMINATED != myThread.getState()) {
// 3. 查看线程状态
System.out.println("3. " + myThread.getName() + " 线程此时的状态为:" + myThread.getState()); // RUNNABLE、TIMED_WAITING
Thread.sleep(500);
}
// 4. 查看线程状态
System.out.println("4. " + myThread.getName() + " 线程此时的状态为:" + myThread.getState()); // TERMINATED
}
}
class MyThread extends Thread {
private boolean flag = true;
@Override
public void run() {
while (flag) {
// 2. 查看线程状态
System.out.println("2. " + Thread.currentThread().getName() + " 线程正在运行,此时的状态为:" + Thread.currentThread().getState()); // RUNNABLE
try {
Thread.sleep(10000);
flag = false;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}生命周期图解
线程的生命周期:
JDK5及之后:

JDK5之前:

易错点总结
核心面试/实战易错点总结:
为了帮你更好地消化,这里对比几个容易混淆的场景:
| 场景区别 | 核心说明 |
|---|---|
BLOCKED vs WAITING | BLOCKED 是因为“别人抢了我的锁,我只能干等”;WAITING 是因为“我自己主动退居幕后,等待别人给我发信号”。 |
sleep() vs wait() | sleep() 进入 TIMED_WAITING,且不释放锁;wait() 进入 WAITING,且必须释放锁,让出临界区资源。 |
JUC 锁 vs synchronized | 使用 ReentrantLock.lock() 等不到锁时,线程进入的是 WAITING 状态(底层是 LockSupport.park()),而不是 BLOCKED。 |
理解了线程的生命周期后,我们在实际开发中往往需要对这些状态进行干预,比如在一个线程处于 WAITING 或 TIMED_WAITING 时安全地打断(interrupt())它。
练习
练习1:新年倒计时:
模拟新年倒计时,每隔1秒输出一个数字,依次输出:10,9,8...1,最后输出:新年快乐!
代码实现:
public class D02E01_HappyNewYear {
public static void main(String[] args) throws InterruptedException {
System.out.println("进入倒计时:");
for (int i = 10; i > 0; i--) {
System.out.println(i);
Thread.sleep(1000);
}
System.out.println("新年快乐!");
}
}练习2:
在main方法中启动两个线程:第1个线程循环随机打印100以内的整数,直到第2个线程从键盘读取了 “Q” 命令。
package p01_create_thread.d04_lifecycle;
import java.util.Scanner;
/**
* 在main方法中启动两个线程:第1个线程循环随机打印100以内的整数,直到第2个线程从键盘读取了 “Q” 命令。
*/
public class D03E02_InterruptThread {
public static void main(String[] args) {
// 1. 创建线程 t1
T1 t1 = new T1();
t1.setName("T1");
t1.start();
// 2. 创建线程 t2
T2 t2 = new T2(t1);
t2.setName("T2");
t2.start();
}
}
// 第一个线程
class T1 extends Thread {
private boolean flag = true;
@Override
public void run() {
while(flag) {
System.out.println((int) (Math.random() * 101));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
// 第二个线程
class T2 extends Thread {
private T1 t1 = null;
public T2(T1 t1) {
this.t1 = t1;
}
@Override
public void run() {
// Scanner
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请输入终止键(Q)结束 T1 线程:");
char c = scanner.next().toUpperCase().charAt(0);
if((char)'Q' == c) {
t1.setFlag(false);
break;
}
}
scanner.close();
}
}